import formencode
from formencode import validators
import md5
from pylons import config
from sqlalchemy import select

try:
  # used to grab the SALT.
  passwords_dict = config['passwords']
except KeyError:
  # We are probably not running in production environment
  from cibynet.config import passwords
  passwords_dict = passwords.load_passwords({'passwords_file': 
                                             'passwords_development.ini'})
  
SALT = passwords_dict['user_pass_salt']
del passwords_dict

import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.associationproxy import association_proxy

from cibynet.model import meta
from cibynet.model import m_rooms
from cibynet.model.m_rooms_users import RoomUser
from cibynet.model.m_users_groups import UserGroup
from cibynet.model import tables as t
from cibynet.model import utils


class User(utils.UpdatableObject):
  """
  Attributes:
    * firstname
    * lastname
    * username
    * email
    * phonenumber
    * roomassociations
    * password: write only, use the validate_password method
    
  Properties:
    * room
    * rooms
    * groups
  """
  
  rooms = association_proxy('roomassociations', 'room', 
                            creator=lambda r, **kw:RoomUser(room=r, **kw))
  groups = association_proxy('groupassociations', 'group', 
                            creator=lambda g, **kw:UserGroup(group=g, **kw))
  
  def _get_roomassoc(self):
    """Returns the last (chronogical) room association of the user.
    """
    return orm.object_session(self).query(RoomUser).filter_by(user_id=self.id).\
    order_by(t.rooms_users.c.from_date.desc()).first()
  
  def _get_room(self):
    """Returns the last room of the user if he is still living in.
       If the user doesn't actually live in a room, returns None.
    """
    assoc = self._get_roomassoc()
    if assoc and not assoc.to_date:
      return assoc.room
    return None
  
  def _set_room(self, room, *args):
    """Set the actual room of the user as the given one, if not already.
    """
    if room is None:
      del self.room
      return
    if self.room is room: 
      return
    if room.user is not None:
      del room.user
    if self.room is not None:
      del self.room
    self.active = True
    self.rooms.insert(0, room)
    
  def _del_room(self):
    """The user leaves the room.
    """
    if self.room:
      self.active = False
      self._get_roomassoc().stop()
    
  room = property(_get_room, _set_room, _del_room)
    
  def __init__(self, firstname, lastname, email, username, password=None,
               phonenumber='', from_old_db=False):
    """Init a new created User.
    
    Arguments:
      - firstname
      - lastname
      - email
      - username
      - password: optional
      - phonenumber: optional
      - from_old_db: optional, default False, put it to True if the user was 
                     imported from the old legacy DB.
    """
    super(User, self).__init__()
    if from_old_db:
      assert username
      firstname = firstname or ""
      lastname = lastname or ""
      email = email or ""
      self.from_old_db = True
    else:
      for e in (firstname, lastname, email, username):
        assert e is not None
    self.firstname = firstname
    self.lastname = lastname
    self.email = email
    self.username = username
    self.first_username = self.username
    if password:
      self.password = password
    if phonenumber:
      self.phonenumber = phonenumber
    self.active = False # Set to True when a room is assigned
    # If you use the roomassociations manually, take care about it yourself!
  
  firstname = utils.UnicodeLowerCaseTitleProperty('_firstname')
  lastname = utils.UnicodeLowerCaseTitleProperty('_lastname')
  email = utils.UnicodeLowerCaseProperty('_email')
  username = utils.UnicodeProperty('_username')
  
  def _get_nice_email(self):
    """Returns the email of the user like: '"Firstname Lastname" <email>'
    """
    return '"%s %s" <%s>' % (self.firstname, self.lastname, self.email)
  nice_email = property(_get_nice_email)
  
  # We only want to be able to set password, not to retrieve it.
  def _set_password(self, password):
    """Set the password (encrypted form)
    """
    self._password = self.__encrypt_password(password)
  password = property(None, _set_password)
  
  def __encrypt_password(self, password, algorithm=md5):
    """Returns the encrypted version of the given password for the user.
    The encrypted form result from the application of the algorithm (ie: md5) on
    the password, the application salt and the first username of the user. 
    
    Arguments:
      - password: string, the non encrypted password of the user
      - algorithm: optional, default to md5. As to be 'duke like' the md5 module
    """
    password = password + SALT + self.first_username
    if isinstance(password, unicode):
      password_8bit = password.encode('UTF-8')
    else:
      password_8bit = password
    hashed_password = algorithm.new(password_8bit).hexdigest()
    # make sure the hased password is an UTF-8 object at the end of the
    # process because SQLAlchemy _wants_ a unicode object for Unicode columns
    if not isinstance(hashed_password, unicode):
      hashed_password = hashed_password.decode('UTF-8')
    return hashed_password
  
  def validate_password(self, password, algorithm=md5):
    """Returns True if the password matched the hashed password in DB.
    
    Arguments: same as __encrypt_password method.
    """
    return self.__encrypt_password(password, algorithm) == self._password
    
  def __repr__(self):
    """
    """
    return "User(%r, %r, %r, %r, %r, %r)" % \
           (self.id, self.firstname, self.lastname, 
                                 self.email, self.username, self.phonenumber)


class UsernameUpdateValidator(validators.String):
  """
  """
  def validate_other(self, value, state):
    """Raise Invalid error if the username is already in use.
    """
    super(validators.String, self).validate_other(value, state)
    userid = int(state.id)
    if utils.GetAllIdsObject(User, username=value) not in [[userid], []]:
      raise validators.Invalid('This user-name is already in use...', 
                               value, state)
username_update_validator = UsernameUpdateValidator(strip=True, not_empty=True)

class UsernameNewValidator(validators.String):
  """
  """
  def validate_other(self, value, state):
    """Raise Invalid error if the username is already in use.
    """
    super(validators.String, self).validate_other(value, state)
    if utils.GetAllIdsObject(User, username=value):
      raise validators.Invalid('This user-name is already in use...', 
                               value, state)
username_new_validator = UsernameNewValidator(strip=True, not_empty=True)


class UserUpdateValidator(formencode.Schema):
  """Validate all the fields used to update an user.
  """
  allow_extra_fields = True
  filter_extra_fields = False
  firstname = validators.String(strip=True, not_empty=True)
  lastname = validators.String(strip=True, not_empty=True)
  username = username_update_validator
  email = validators.Email(strip=False, not_empty=True) #resolve_domain=True)
  room = m_rooms.RoomIDValidator(not_empty=False)
  password = validators.String(min=5, not_empty=False)
  passwordconfirmation = validators.String(min=5, not_empty=False)
  phonenumber = validators.String(min=10, strip=True, not_empty=False)
  chained_validators = [
          validators.FieldsMatch('password', 'passwordconfirmation'),
          ]
  

class UserNewValidator(UserUpdateValidator):
  """Validate all the fields used to create an user.
  """
  password = validators.String(min=5, not_empty=True)
  passwordconfirmation = validators.String(min=5, not_empty=True)
  username = username_new_validator
  room = m_rooms.RoomIDValidator(not_empty=True)
